branded types
「branded types」という名前はTypeScript特有の概念のようで他での実例は見かけないmrsekut.icon
一方で、TypeScriptの文脈ではこの名前がよく使われる
compile後の構造に影響を与えないことが特徴
定義の例
code:ts
type Branded<T, Name extends string> = T & { [key in __${Name}]: typeof tag };
declare const tag: unique symbol;
[key in \`__${V}\`の意味
keyにVを使うことで、Brandedで定義した型を組み合わすことができる
code:ts
type A = Branded<number, 'a'>;
type B = Branded<A, 'b'>; // BrandedのBranded
ここが固定されたpropertyになっていると、組み合わすことが出来ない
例えば{ _type: U }にしていると、
code:ts
type B = number & { _type: 'a' } & { _type: 'b' } // never
propertyが衝突しているため、そういう型は存在せずneverになる
__を付けているのは、意図せず同じような型が定義されることを避けるため
「このprojectでは__から始まるpropertyは使わない」という前提があるmrsekut.icon
使用例
code:ts
type UserId = Branded<number, 'UserId'>;
const mkUserId = (n: number) => {
return n as UserId;
};
type GenreId = Branded<number, 'GenreId'>;
const mkGenreId = (n: number) => {
return n as GenreId;
};
const doSomething = (id: UserId) => {
console.log(id);
};
const id1 = mkUserId(10);
const id2 = 11;
const id3 = mkGenreId(12);
doSomething(id1);
doSomething(id2); // error
doSomething(id3); // error
当然、"UserId"という名前が被った場合は同等のものになるということに注意する
code:ts
// 以下は同値
type UserId1 = Branded<number, 'UserId'>;
type UserId2 = Branded<number, 'UserId'>;
これは構造的部分型の限界ではあるが、運用でカバーしましょう
Brandedの定義例は無限に思いつくので好きなように定義すれば良い
要は、「そんな型、自分で定義することないっしょ」という構造にしておけば良い
例えば、以下のような型はもしかしたら問題になるかもしれない
code:ts
type Branded<T, U extends string> = T & { _type: U };
当然以下の2つの型定義は区別されない
code:ts
type UserId1 = Branded<number, 'UserId'>
type UserId2 = number & { _type: 'UserId' }
プロジェクト内で意図せずUserId2のような定義をすると区別されないことになる
「_から始まるpropertyは使わない」のようなルールがチーム内にあれば、これでも問題になることはない
その塩梅は適当で良い
型を厳格に扱う系のlibraryでは用意されていることが多い
zodを使うprojectではこれを使えば良いと思うmrsekut.icon
組み合わすとneverになってしまう